— 2 min read
Spring batch를 쓰다보면 ItemWriter에 void write(List<? extends T> var1)
이런 메서드가 있어 왜 저런 제네릭 타입을 쓰는걸까 궁금했었는데 이제서야 찾아보게 되었다.
contravariance 개념에 대한 글들을 봐도 뭔가 직관적이지 않아서 이해하는데에만 몇 시간이나 걸렸는데, 이해하고 나니 어찌보면 단순하다. 최대한 정리해봤다.
처음엔 일단 뭐라고 검색해야할지 명칭조차 까먹어서 다시 정리를 해봤다. 이펙티브 자바에 나오는 용어 기준이다.
?
: wildcard. unknown type을 나타낸다List<?>
: unbounded wildcard type(비한정적 와일드카드 타입)List<? extends Integer>
, List<? super Integer>
: bounded wildcard type(한정적 와일드카드 타입)? super Integer
: Integer이거나 Integer의 supertype이란 뜻? extends Integer
: Integer이거나 Integer의 subtype이란 뜻E
: formal type parameter(정규타입 매개변수)List<E>
: generic type1interface Animal {2 void eat()3}4class Panda extends Animal {5 void eat()6}
Panda는 Animal의 하위 타입이지만, List<Panda>
는 List<Animal>
의 하위타입이 아니다.
List<Panda>
는 List<Animal>
이 하는 일 (온갖 종류의 Animal 타입을 add하기)를 할 수 없기 때문(Panda타입만 add할 수 있다)⇒ 클래스의 상속관계가 Generics에서는 상속관계로 유지되지 않는 것을 Invariance라고 한다 Generics는 컴파일 단계에서 Generics의 타입이 지워지기 때문. 예시에서 JVM은 Runtime에 List 객체만 알고 있게 된다.
아래 코드와 같은 상황이 컴파일 가능하려면, Invariance로는 안된다.
1void copyAll(Collection<Object> to, Collection<String> from) {2 to.addAll(from); 3}
⇒ 이런 상황에서 유연성을 극대화하기 위해 bounded wildcard (한정적 와일드카드) 타입을 사용한다.
? extends T
(Kotlin: <out T>
)
String이 Object의 하위타입이니 Collection<String>
도 Collection<? extends Object>
의 하위타입으로 쓸 수 있다
List<? extends T>에는 read(get) 만 할수있고, add는 할 수 없다. (이유는 밑에서 설명)
1List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);2List<? extends Number> numbers = doubles; // ok34Number number = numbers.get(0);5System.out.println(number);6numbers.add(1.1); // compile error
? super T
(Kotlin: <in T>
)
Integer가 Number의 하위타입 → Collection<Number>
를 Collection<? super Integer>
의 하위타입으로 쓸 수 있다
List<? super T>
에는 read(get)은 할 수 없고, add는 할 수 있다. (이유는 밑에서 설명)
1public void addNumber(List<? super Integer> numbers) {2 numbers.add(6);3 // numbers.get(0); 컴파일 에러4}56List<Number> myInts = new ArrayList<>();7addNumber(myInts);89System.out.println(myInts); // 정상
<? extends T>
와 <? super T>
를 각각 언제 써야할까?
이펙티브 자바에서는 PECS를 기억하면 된다고 소개하고 있다.
<? extends T>
, 소비자라면 <? super T>
를 써야한다는 뜻.List<T>
라면 'List의 관점'에서 봐야 한다.1public T method1() {} // ok2public <? extends T> method2() {} // nope!
PECS 원칙은 알겠는데, 왜 원칙이 이렇게 되었는지가 궁금했다.
우선 기억해야할 것은
covariance 예시를 다시 보자.
1List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);2List<? extends Number> numbers = doubles; // ok34Number number = numbers.get(0); 5 // numbers에서 가져온 어떤 객체이든 Number타입이거나 6 // Number타입으로 upcasting 되므로 compile 가능하다78numbers.add(1.1); // compile error9 // Number보다 하위인 Double이라서 왜 안되는지 의아할 수 있지만,10 // Double보다 더 하위 클래스가 List에 포함된 상태일 수도 있기 때문에 11 // Double이 들어가서 type이 safe함을 보장하지 못한다.
1public void addNumber(List<? super Integer> numbers) {2 numbers.add(6); // Integer과 Integer의 super타입을 저장하는 List니까, 3 // Integer타입을 add하는 것은 가능하다.4 5 int a = numbers.get(0); // 컴파일 에러6 // 부모클래스도 같이 저장되어있으므로7 // Number가 아닌 Integer를 get 해올 수 있다는 보장이 없다. 8}
covariance-contravariance는 각각 다른 개념이 아니라 같은 이유로부터 나온 개념이다.
Collection<T>
로부터 T를 꺼내올 때, Collection<T>
는 생산자. Collection<? extends T>
로 유연하게 만들면 read-only가 된다.
Collection<T>
에 T를 더 넣을 때, Collection<T>
는 소비자이며 Collection<? super T>
로 만들면 write-only가 된다.